package com.lx.boot.lock;

import com.lx.boot.OS;
import com.lx.util.LX;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@Slf4j
public class RedisLock {

    private StringRedisTemplate stringRedisTemplate;

    /**
     * 默认锁的有效时间(s)
     */
    public static final int EXPIRE = 30;

    /**
     * 加/解锁的lua脚本
     */
    public static final String LOCK_LUA;
    public static final String UNLOCK_LUA;
    static {
        //判断key是否存在 判断值对不对 对+1
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call('exists',KEYS[1]) == 0 ");
        sb.append("then ");
        sb.append("    redis.call('hincrby' ,KEYS[1] ,ARGV[1] ,1) ");
        sb.append("    redis.call('expire' ,KEYS[1] ,ARGV[2]) ");
        sb.append("    return 1 ");
        sb.append("end ");
        sb.append("if redis.call('hexists',KEYS[1], ARGV[1]) == 1 ");
        sb.append("then ");
        sb.append("    redis.call('hincrby' ,KEYS[1] ,ARGV[1] ,1) ");
        sb.append("    redis.call('expire' ,KEYS[1] ,ARGV[2]) ");
        sb.append("    return 1 ");
        sb.append("end ");
        sb.append("return 0 ");
        LOCK_LUA = sb.toString();


        //判断值是否一致 次数减少  如果次数为0 则删除键
        sb = new StringBuilder();
        sb.append("if redis.call('hexists',KEYS[1], ARGV[1]) == 0 then ");
        sb.append("return 0 ");
        sb.append("end ");
        sb.append("if redis.call('hincrby' ,KEYS[1] ,ARGV[1] ,-1) > 0 then ");
        sb.append("    redis.call('expire' ,KEYS[1] ,ARGV[2]) ");
        sb.append("else ");
        sb.append("    redis.call('del' ,KEYS[1])");
        sb.append("end ");
        sb.append("return 1 ");
        UNLOCK_LUA = sb.toString();
    }

    /**
     * 锁标志对应的key
     */
    private String lockKey;
    /**
     * 锁对应的值
     */
    private String lockValue;
    /**
     * 锁的有效时间(s)
     */
    private int expireTime = EXPIRE;

    final Random random = new Random();
    public int getExpireTime() {
        return expireTime;
    }
    public void setExpireTime(int expireTime) {
        this.expireTime = expireTime;
    }

    /**
     * 使用默认的锁过期时间和请求锁的超时时间
     *
     * @param redisTemplate
     * @param lockKey       锁的key（Redis的Key）
     */
    RedisLock(StringRedisTemplate stringRedisTemplate, String lockKey) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockKey = lockKey + "_lock";
        this.lockValue = OS.getLogTraceId();
        if (this.lockValue == null){
            this.lockValue = LX.uuid32(10);
        }
    }

    /**
     * 使用默认的请求锁的超时时间，指定锁的过期时间
     *
     * @param redisTemplate
     * @param lockKey       锁的key（Redis的Key）
     * @param expireTime    锁的过期时间(单位：秒)
     */
    RedisLock(StringRedisTemplate stringRedisTemplate, String lockKey, int expireTime) {
        this(stringRedisTemplate, lockKey);
        this.expireTime = expireTime;
    }

    /**
     * 尝试获取锁 超时返回
     * @return
     */
    public boolean tryLock(long timeout) {
        // 请求锁超时时间，纳秒
        long _timeout = timeout * 1000000;
        // 系统当前时间，纳秒
        long nowTime = System.nanoTime();
        while ((System.nanoTime() - nowTime) < _timeout) {
            if (this.lock()) {
                // 上锁成功结束请求
                return true;
            }
            // 每次请求等待一段时间
            sleep(100, 50000);
        }
        return false;
    }

    /**
     * 尝试获取锁 立即返回
     * @return 是否成功获得锁
     */
    public boolean lock() {
        List<String> keys = new ArrayList<>();
        keys.add(lockKey);
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(LOCK_LUA);
        redisScript.setResultType(Long.class);
        Long result = (Long) stringRedisTemplate.execute(redisScript, keys, lockValue , expireTime+"");
        if (result == 1){
            log.info("分布式锁-加锁:"+lockKey+" "+stringRedisTemplate.opsForHash().get(lockKey,lockValue));
        }
        return result == 1;
    }

    /**
     * 以阻塞方式的获取锁
     *
     * @return 是否成功获得锁
     */
    public boolean lockBlock() {
        while (true) {
            //不存在则添加 且设置过期时间（单位ms）
            if (lock()) {
                return true;
            }
            // 每次请求等待一段时间
            sleep(100, 50000);
        }
    }

    /**
     * 解锁
     */
    public boolean unlock() {
        List<String> keys = new ArrayList<>();
        keys.add(lockKey);
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(UNLOCK_LUA);
        redisScript.setResultType(Long.class);
        Long result = (Long) stringRedisTemplate.execute(redisScript, keys, lockValue , expireTime+"");
        if (result == 1) {
            log.info("分布式锁-解锁:" + lockKey + " " + stringRedisTemplate.opsForHash().get(lockKey,lockValue));
        }
        return result == 1;
    }

    /**
     * 线程等待时间
     *
     * @param millis 毫秒
     * @param nanos  纳秒
     */
    private void sleep(long millis, int nanos) {
        try {
            Thread.sleep(millis, random.nextInt(nanos));
        } catch (InterruptedException e) {
            log.info("获取分布式锁休眠被中断：", e);
            Thread.currentThread().interrupt();
        }
    }

}
